﻿/*
 * Copyright © 2011 Tristan Reeves, treeves@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using Spring.Objects.Factory.Config;
using Spring.Objects.Factory.Support;
using Spring.Core;
using Sprinkles.Secondary;
using Spring.Context;
using Sprinkles.Filters;

namespace Sprinkles.Hook
{
    public class FluentHook : IObjectFactoryPostProcessor, IObjectPostProcessor, IApplicationContextAware, IOrdered
    {
        private readonly AssemblyScanningDeploymentHostProvider m_DeploymentHostProvider = new AssemblyScanningDeploymentHostProvider();
        private IApplicationContext m_ApplicationContext;
        private string m_UserContext = "";
        private IConfigurableListableObjectFactory m_Factory;
        private readonly Dictionary<IObjectDefinition, List<Action<object>>> m_PreinitCallbacks = new Dictionary<IObjectDefinition, List<Action<object>>>();
        private readonly Dictionary<IObjectDefinition, List<Action<object>>> m_PostinitCallbacks = new Dictionary<IObjectDefinition, List<Action<object>>>();
        private object PostProcessCommon(object instance, string objectName, Dictionary<IObjectDefinition, List<Action<object>>> callbacksMap)
        {
            var def = m_Factory.GetObjectDefinition(objectName);

            List<Action<object>> callbacks;
            if (!callbacksMap.TryGetValue(def, out callbacks)) return instance;
            foreach (var callback in callbacks)
            {
                callback(instance);
            }
            return instance;
        }
        private ObjectDefinitionBuilder CreateObjectDefinitionBuilder(DeploymentInfo info, Dictionary<Type, string> defaultMaps)
        {
            ObjectDefinitionBuilder odb;
            if (!string.IsNullOrEmpty(info.StaticFactoryMethodName))
            {
                odb = ObjectDefinitionBuilder.GenericObjectDefinition(info.StaticFactoryMethodType);
                odb.SetFactoryMethod(info.StaticFactoryMethodName);
            }
            else if (!string.IsNullOrEmpty(info.InstanceFactoryMethodNameFunc))
            {
                odb = ObjectDefinitionBuilder.GenericObjectDefinition();
                var refName = info.InstanceFactoryObjectName();
                odb.SetFactoryObject(refName, info.InstanceFactoryMethodNameFunc);
            }
            else
            {
                odb = ObjectDefinitionBuilder.GenericObjectDefinition(info.Type);
            }

            if (!info.IsSingleton) odb.SetSingleton(info.IsSingleton);
            if (info.IsSingleton) odb.SetLazyInit(info.IsLazyInit);
            if (info.InitMethod != null) odb.SetInitMethodName(info.InitMethod);
            if (info.DestroyMethod != null) odb.SetDestroyMethodName(info.DestroyMethod);
            if (info.IsAbstract) odb.SetAbstract(info.IsAbstract);
            if (info.ParentNameFunc != null)
            {
                var parentName = (info.ParentNameFunc() ?? "").Trim();
                if (parentName != "") odb.RawObjectDefinition.ParentName = parentName;
            }
            if (info.Scope != Scope.None) odb.RawObjectDefinition.Scope = info.Scope.ToSpringScope();

            foreach (var dependsOnRef in info.DependsReferences)
            {
                odb.AddDependsOn(dependsOnRef);
            }

            foreach (var methodOverride in info.MethodOverrides)
            {
                var refName = methodOverride.ObjectReference();
                var ov = new LookupMethodOverride(methodOverride.MethodName, refName);
                odb.RawObjectDefinition.MethodOverrides.Add(ov);
            }

            foreach (var kv in info.PropertyCombinations)
            {
                var nam = kv.Key.Name;
                var val = kv.Value;
                if (val is IFutureDeployedObject)
                {
                    var refName = ((IFutureDeployedObject)val).GetName();
                    var ror = new RuntimeObjectReference(refName);
                    odb.AddPropertyValue(nam, ror);
                }
                else if (val is SprinklesExpressionHolder)
                {
                    var exh = new ExpressionHolder(((SprinklesExpressionHolder)val).Expression);
                    odb.AddPropertyValue(nam, exh);
                }
                else if (val is SprinklesRuntimeObjectReference)
                {
                    var ror = new RuntimeObjectReference(((SprinklesRuntimeObjectReference)val).Reference);
                    odb.AddPropertyValue(nam, ror);
                }
                else if (val is AutoReference)
                {
                    var t = kv.Key.PropertyType;
                    string refName = null;
                    if (!defaultMaps.TryGetValue(t, out refName))
                    {
                        throw new InvalidOperationException("Cannot find a default for property [" + info.Name + "].[" + kv.Key.Name + "]");
                    }
                    var ror = new RuntimeObjectReference(refName);
                    odb.AddPropertyValue(nam, ror);
                }
                else
                {

                    odb.AddPropertyValue(nam, val);
                }
            }

            var args = info.ConstructorArgs;
            foreach (var arg in args)
            {
                var construcrorArgList = odb.RawObjectDefinition.ConstructorArgumentValues;
                var nam = arg.Name;
                var val = arg.Value;

                if (val is IFutureDeployedObject)
                {
                    val = new RuntimeObjectReference(((IFutureDeployedObject)val).GetName());
                }
                else if (val is SprinklesExpressionHolder)
                {
                    val = new ExpressionHolder(((SprinklesExpressionHolder)val).Expression);
                }
                else if (val is SprinklesRuntimeObjectReference)
                {
                    val = new RuntimeObjectReference(((SprinklesRuntimeObjectReference)val).Reference);
                }
                else if (val is AutoReference)
                {
                    var t = arg.DeclaredType;
                    string refName = null;
                    if (!defaultMaps.TryGetValue(t, out refName))
                    {
                        throw new InvalidOperationException("Cannot find a default for constructor arg [" + info.Name + "](" + arg.Name + ")");
                    }
                    val = new RuntimeObjectReference(refName);
                }

                construcrorArgList.AddNamedArgumentValue(nam, val);
            }
            return odb;
        }
        private IEnumerable<IDeploymentHost> GetDeploymentHosts()
        {
            var hosts = m_DeploymentHostProvider.GetDeploymentHosts().ToArray();

            foreach (var dh in hosts)
            {
                dh.SetUserContext(UserContext);
                dh.SetContainerContext(m_ApplicationContext);
                dh.SetObjectRetrievingCallback(this.GetObjectCallback);
                yield return dh;
            }
        }
        private object GetObjectCallback(string name)
        {
            return m_ApplicationContext[name];
        }

        public object PostProcessAfterInitialization(object instance, string objectName)
        {
            var result = PostProcessCommon(instance, objectName, m_PostinitCallbacks);
            return result;
        }
        public object PostProcessBeforeInitialization(object instance, string objectName)
        {
            var result = PostProcessCommon(instance, objectName, m_PreinitCallbacks);
            return result;
        }
        public void PostProcessObjectFactory(IConfigurableListableObjectFactory factory)
        {
            m_Factory = factory;
            var hosts = GetDeploymentHosts().ToArray();

            var deployments =
            (
                from h in hosts
                from d in h.GetDeployments()
                select new { Deployment = d, Info = d.ExtractInfo() }
            ).ToArray();

            //all registered aliases
            var globalAliases = new Dictionary<string, Func<string>>();
            foreach (var host in hosts)
            {
                var localAliases = host.GetAliases();
                foreach (var localAlias in localAliases)
                {
                    globalAliases.Add(localAlias.Key, localAlias.Value);
                }
            }

            //all registered defaultMaps
            var globalDefaultMappings = new Dictionary<Type, Func<string>>();
            foreach (var host in hosts)
            {
                var localMappings = host.GetDefaultMappings();
                foreach (var localMapping in localMappings)
                {
                    globalDefaultMappings.Add(localMapping.Key, localMapping.Value);
                }
            }

            //make sure everything is 'done' first
            foreach (var diPair in deployments)
            {
                var d = diPair.Deployment;
                var info = diPair.Info;
                if (!d.IsFinished) throw new InvalidOperationException("Deployment not complete for [" + info.Name + "]. Please call the Done() method");
            }

            //normalise names as much as possible
            var factoryNames = factory.GetObjectDefinitionNames();
            var existing_names = new List<string>(factoryNames.Select(x => x.ToLower()));
            foreach (var diPair in deployments)
            {
                var d = diPair.Deployment;
                var info = diPair.Info;

                //make sure we get a unique name, and hand it back to the deployment object
                var proposedName = info.Name;
                var ndx = 0;
                while (existing_names.Contains(proposedName.ToLower()))
                {
                    ndx++;
                    proposedName = info.Name + "." + ndx;
                }
                d.ChangeNameIfNecessary(proposedName);
                info.ChangeNameIfNecessary(proposedName);
                existing_names.Add(proposedName.ToLower());
            }

            //create the defualt mapping table
            var defaultMaps = new Dictionary<Type, string>();

            //populate from global declarations
            foreach (var m in globalDefaultMappings)
            {
                defaultMaps.Add(m.Key, m.Value());
            }
            const string msgTemplate = "The Type [{0}] is registered as a default twice: once for [{1}], and once for [{2}]";
            foreach (var diPair in deployments)
            {
                var info = diPair.Info;
                var types = info.DefaultForTypes ?? new Type[0];
                foreach (var t in types)
                {
                    if (defaultMaps.ContainsKey(t))
                    {
                        var msg = string.Format(msgTemplate, t, defaultMaps[t], info.Name);
                        throw new InvalidOperationException(msg);
                    }
                    defaultMaps.Add(t, info.Name);
                }
            }

            //setup aliases
            foreach (var alias in globalAliases)
            {
                var val = alias.Value();
                factory.RegisterAlias(val, alias.Key);
            }


            //do the main loop
            foreach (var diPair in deployments)
            {
                var d = diPair.Deployment;
                var info = diPair.Info;

                var odb = CreateObjectDefinitionBuilder(info, defaultMaps);
                var def = odb.ObjectDefinition;

                m_PreinitCallbacks.Add(def, info.PreinitCallbacks.ToList());
                m_PostinitCallbacks.Add(def, info.PostinitCallbacks.ToList());
                factory.RegisterObjectDefinition(info.Name, def);
            }

            //handle object-factory postprocessors
            var objectFactoryPostProcessors = new List<IObjectFactoryPostProcessor>();
            foreach (var diPair in deployments)
            {
                var d = diPair.Deployment;
                var info = diPair.Info;

                //if (typeof(IObjectPostProcessor).IsAssignableFrom(info.Type))
                //{
                //    var opp = factory.GetObject(info.Name) as IObjectPostProcessor;
                //    m_Factory.AddObjectPostProcessor(opp);
                //}

                if (typeof(IObjectFactoryPostProcessor).IsAssignableFrom(info.Type))
                {
                    var ofpp = factory.GetObject(info.Name) as IObjectFactoryPostProcessor;
                    if (ofpp != null) objectFactoryPostProcessors.Add(ofpp);
                }
            }

            objectFactoryPostProcessors = objectFactoryPostProcessors
                .OrderBy(x => x is IOrdered ? ((IOrdered)x).Order : int.MaxValue)
                .ThenBy(x => x.GetType().AssemblyQualifiedName)
                .ToList();
            foreach (var ofpp in objectFactoryPostProcessors)
            {
                ofpp.PostProcessObjectFactory(factory);
            }

        }

        public FluentHook()
        {
        }
        public string[] AssembliesToScan
        {
            get { return m_DeploymentHostProvider.AssembliesToScan; }
            set { m_DeploymentHostProvider.AssembliesToScan = value; }
        }
        public bool ReadAttributeBasedDeployments
        {
            get { return m_DeploymentHostProvider.ReadAttributeBasedDeployments; }
            set { m_DeploymentHostProvider.ReadAttributeBasedDeployments = value; }
        }
        public bool ReadDeploymentHosts
        {
            get { return m_DeploymentHostProvider.ReadDeploymentHosts; }
            set { m_DeploymentHostProvider.ReadDeploymentHosts = value; }
        }
        public bool Autoscan
        {
            get { return m_DeploymentHostProvider.Autoscan; }
            set { m_DeploymentHostProvider.Autoscan = value; }
        }
        public string UserContext
        {
            get { return m_UserContext; }
            set { m_UserContext = (value ?? "").Trim(); }
        }
        public int Order { get; set; }
        public IApplicationContext ApplicationContext
        {
            set
            {
                m_ApplicationContext = value;
            }
        }
        public IDeploymentHostProvider DeploymentHostProvider
        {
            get { return m_DeploymentHostProvider; }
        }
    }
}